/*
Copyright (c) 2008-2009, Oleg Estekhin
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name of the copyright holders nor the names of its
      contributors may be used to endorse or promote products derived from this
      software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package fractal.math.fractal;

import java.util.Collections;
import java.util.Map;

import oe.math.complex.CalculationContext;
import oe.math.complex.Complex;
import oe.math.complex.Expression;

/** The escape-time fractal. */
public final class Fractal {

    private final String initialPointName;

    private final Expression initialExpression;

    private final String iterationValueName;

    private final Expression iterationExpression;

    private final double escapeRadius;

    private final int maxIteration;


    /**
     * Creates a new {@code Fractal} object.
     *
     * @param initialPointName the name of the variable that represents the initial point
     * @param initialExpression the expression for the initial value
     * @param iterationValueName the name of the variable that represents the iteration value
     * @param iterationExpression the expression for the next value
     * @param escapeRadius the escape radius
     * @param maxIteration the maximum iteration
     *
     * @throws NullPointerException if {@code initialPointName}, {@code initialExpression}, {@code iterationPointName}
     * or {@code iterationExpression} is {@code null}
     * @throws IllegalArgumentException if {@code initialPointName} or {@code iterationPointName} is not a legal
     * variable name
     * @throws IllegalArgumentException if {@code escapeRadius} is negative
     * @throws IllegalArgumentException if {@code maxIteration} is negative
     */
    public Fractal( String initialPointName, Expression initialExpression, String iterationValueName, Expression iterationExpression, double escapeRadius, int maxIteration ) {
        if ( initialExpression == null ) {
            throw new NullPointerException( "initialExpression is null" );
        }
        if ( iterationExpression == null ) {
            throw new NullPointerException( "iterationExpression is null" );
        }
        if ( escapeRadius < 0.0 ) {
            throw new IllegalArgumentException( "escapeRadius is negative" );
        }
        if ( maxIteration < 0 ) {
            throw new IllegalArgumentException( "maxIteration is negative" );
        }
        this.initialPointName = initialPointName;
        this.initialExpression = initialExpression;
        this.iterationValueName = iterationValueName;
        this.iterationExpression = iterationExpression;
        this.escapeRadius = escapeRadius;
        this.maxIteration = maxIteration;
    }


    /**
     * Returns the name of the variable that represents the initial point.
     *
     * @return the initial point name
     */
    public String getInitialPointName() {
        return initialPointName;
    }

    /**
     * Returns the complex arithmetic expression for the initial sequence value.
     *
     * @return the expression for the initial value
     */
    public Expression getInitialExpression() {
        return initialExpression;
    }

    /**
     * Returns the name of the variable that represents the iteration value.
     *
     * @return the iteration point name
     */
    public String getIterationValueName() {
        return iterationValueName;
    }

    /**
     * Returns the complex arithmetic expression for the next sequence value.
     *
     * @return the expression for the next value
     */
    public Expression getIterationExpression() {
        return iterationExpression;
    }

    /**
     * Returns the escape radius.
     *
     * @return the escape radius
     */
    public double getEscapeRadius() {
        return escapeRadius;
    }

    /**
     * Returns the maximum iteration to be used in calculations.
     *
     * @return the maximum iteration
     */
    public int getMaxIteration() {
        return maxIteration;
    }


    /**
     * Calculates the escape iteration for the specified initial point.
     *
     * @param initialPoint the initial point value
     *
     * @return the escape iteration
     *
     * @throws NullPointerException if {@code initialPoint} is {@code null}
     * @throws InterruptedException if the current thread is interrupted
     */
    public int calculateEscapeIteration( Complex initialPoint ) throws InterruptedException {
        return calculateEscapeIteration( initialPoint, Collections.<String, Complex>emptyMap() );
    }

    /**
     * Calculates the escape iteration for the specified initial point. The values of additional variables will be taken
     * from the specified map.
     *
     * @param initialPoint the initial point value
     * @param values a mapping from the variable name to the variable value
     *
     * @return the escape iteration
     *
     * @throws NullPointerException if {@code initialPoint} or {@code values} is {@code null}
     * @throws InterruptedException if the current thread is interrupted
     */
    public int calculateEscapeIteration( Complex initialPoint, Map<String, Complex> values ) throws InterruptedException {
        if ( initialPoint == null ) {
            throw new NullPointerException( "initialPoint is null" );
        }
        if ( values == null ) {
            throw new NullPointerException( "values is null" );
        }
        CalculationContext context = new CalculationContext();
        context.setVariable( initialPointName, initialPoint );
        int iteration = 0;
        Complex iterationValue = initialExpression.calculate( context );
        while ( iteration < maxIteration && iterationValue.getAbs() <= escapeRadius ) {
            if ( Thread.interrupted() ) {
                throw new InterruptedException();
            }
            iteration++;
            if ( iteration < maxIteration ) {
                context.setVariable( iterationValueName, iterationValue );
                iterationValue = iterationExpression.calculate( context );
            }
        }
        return iteration;
    }


    @Override
    public int hashCode() {
        int result = 17;
        result = 37 * result + initialPointName.hashCode();
        result = 37 * result + initialExpression.hashCode();
        result = 37 * result + iterationValueName.hashCode();
        result = 37 * result + iterationExpression.hashCode();
        long escapeRadiusL = Double.doubleToLongBits( escapeRadius );
        result = 37 * result + ( int ) ( escapeRadiusL ^ escapeRadiusL >>> 32 );
        result = 37 * result + maxIteration;
        return result;
    }

    @Override
    public boolean equals( Object obj ) {
        if ( this == obj ) {
            return true;
        } else if ( obj instanceof Fractal ) {
            Fractal fractal = ( Fractal ) obj;
            return initialPointName.equals( fractal.initialPointName )
                    && initialExpression.equals( fractal.initialExpression )
                    && iterationValueName.equals( fractal.iterationValueName )
                    && iterationExpression.equals( fractal.iterationExpression )
                    && Double.doubleToLongBits( escapeRadius ) == Double.doubleToLongBits( fractal.escapeRadius )
                    && maxIteration == fractal.maxIteration;
        } else {
            return false;
        }
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append( "Fractal[" )
                .append( "initialPointName=" ).append( initialPointName )
                .append( ',' )
                .append( "initialExpression=" ).append( initialExpression )
                .append( ',' )
                .append( "iterationValueName=" ).append( iterationValueName )
                .append( ',' )
                .append( "iterationExpression=" ).append( iterationExpression )
                .append( ',' )
                .append( "escapeRadius=" ).append( escapeRadius )
                .append( ',' )
                .append( "maxIteration=" ).append( maxIteration )
                .append( ']' );
        return buffer.toString();
    }

}
